#!/usr/bin/perl -w
#===============================================================================
#   联系方式:zhang.dongliang@zte.com.cn
#
#   使用说明：
#       -f value  指定需要处理的单个文件
#       -p value  指定需要处理的路径（路径下文件根据 -r 制定的正则表达式进行匹配，
#                 没有 -r 参数则处理所有目录下文件，但是不进行目录的递归
#                 注意：-f 与 -p/-r 参数同时存在只取 -f 参数的值）
#       -s value  指定 -p 参数匹配文件的正则表达式，可以为空，则代表匹配目录下所
#                 有文件
#       -g value  指定需要匹配的具体属性键值（key=value）
#       -v value  指定需要排斥的搜索项，即即使话单中含有-g参数内容如果含有-v内容
#                 也不匹配
#       -a value  指定需要添加的具体属性键值（key=value）
#                 value 可以支持$1,$2代表已经存在的属性(1=$2)
#       -r value  指定需要替换的具体属性键值（key=new_value，
#                 可以与 -g 参数不一致，但是删除只在-g参数匹配的话单内进行替换）
#                 new_value 可以支持$1,$2代表已经存在的属性
#       -d value  指定需要删除的具体属性键值（key=value，
#                 可以与 -g 参数不一致，但是删除只在-g参数匹配的话单内进行删除
#                 如果value为空，则不校验value是否符合，直接根据key删除）
#       -t value  将结果输出到标准输出，可以不指定 -o 参数
#                   （0-将28格式转化为一列输出“默认”；1-按照28格式输出；off-关闭屏幕输出）
#                   注意如果没有添加value请不要在其后添加别的参数
#       -o value  指定输出文件的目录
#       -l value  指定日志等级
#       -O  对于文件中未匹配到的话单也直接进行输出，指定后-g参数不再有作用
#       -h  打印帮助信息
#
#   高级使用：
#       -F funtion 指定需要调用的函数，可以在匹配到的cdr中应用这个函数，支持perl!
#                  t28.pl -f filename.s -g '1=12345' -t off -F f.pl
#                  例子如下：
#                  因传入的为指针，可以采用$$$cdr_list_ref{42}访问指定属性
#                  可以在指定的插件文件中定义新的函数，但是请以proc_开头，以免与脚本中其他函数冲突
#-------------------------------------------------------------------------------
##f.pl
#sub proc_main {
#    my ($cdr_list_ref) = @_;
#    #--从这里开始可以修改一下内容--
#    print '42=',$$$cdr_list_ref{42},"\n" if (exists $$$cdr_list_ref{42});
#    print '43=',$$$cdr_list_ref{43},"\n" if (exists $$$cdr_list_ref{43});
#    print '44=',$$$cdr_list_ref{44},"\n" if (exists $$$cdr_list_ref{44});
#    print '45=',$$$cdr_list_ref{45},"\n" if (exists $$$cdr_list_ref{45});
#    #--可修改内容到此结束--
#}
#1;
#-------------------------------------------------------------------------------
#
#   注意：  请用单引号将键值对包裹起来，多个字段用"|"分隔，
#           如： -g '1=15312002990|2=asdf'
#
#   例子：
#       1，从单个文件中查找符合需求的匹配，以行方式打印在屏幕上：
#           t28.pl -f filename.s -g '1=12345' -t
#
#       2，从路径下s结尾的文件中查找，以28格式打印在屏幕上：
#           t28.pl -p pathname -s 's$' -g '1=12345' -t 1
#           注意：-s参数中需要填入的是Perl的正则表示，而非普通的 *.?
#
#       3，将查找到的文件输出到指定路径下，程序会自动在文件名后添加".new"后缀：
#           t28.pl -p pathname -s 's$' -g '1=12345' -o outpathname
#
#       3，对查找到的话单进行替换/新增/删除属性操作，查找的属性可以与需要操作
#          的属性不同：
#           t28.pl -p pathname -s 's$' -g '1=12345' -r '3=0987655' -t
#           t28.pl -p pathname -s 's$' -g '1=12345' -a '2=0987655' -t
#           t28.pl -p pathname -s 's$' -g '1=12345' -d '5=0987655' -t
#
#       4，使用 -v 参数排除 -g 的部分选择：
#           t28.pl -p pathname -s 's$' -g '1=12345' -v '3=0987655' -t
#
#       5，多个参数的匹配与替换：
#           t28.pl -p pathname -s 's$' -g '1=12345|2=34543|3=aaa' -r '3=0987655|4=sdfasf' -t 1
#
#TODO:
#   4，进行效率的优化
#   5，改写为面向对象的，同时需要支持可以当作一个库使用--暂时不做
#===============================================================================

use strict;
use Getopt::Std;
use Cwd;
use File::Spec;
use POSIX qw(strftime);

#===============================================================================

# 定义需要处理的文件默认换行方式
local $/ = "\n";
# 用于保存输入的命令行参数
our %opts;
# 初始目录
our $init_dir = getcwd;
# 打印日志等级 0,3,5
our $log_level = 0;
# 解析多替换参数的变量
our (@param_g, @param_a, @param_r, @param_d, @param_v);
#===============================================================================
#支持的外挂处理程序

#===============================================================================
#获取当前时间，并且按照指定进行输出
sub get_time{
    my $now_string = strftime "%Y%m%d%H%M%S", localtime;
}

# 日志处理函数
sub mylog{
    my ($line_number, $level, $tag, $msg) = @_;
    my $now_string = get_time();
    
    $line_number='NULL' if !defined $line_number;
    $tag='unknow_tag' if !defined $tag;
    $level='log' if !defined $level;
    
    if($level eq 'die'){
        print "[$now_string|$tag|$line_number]$msg\n";
        exit -1;
    }elsif($level eq 'warn' or int($level) <= $log_level){
        print "[$now_string|$tag|$line_number]$msg\n";
    }
}

# 使用方法
sub usage{
    if(exists $opts{'h'} || (scalar keys %opts == 0)){
        print <<END;
    usage:
       -f  the file of need transform
       -p  the dir of need transform
       -s  the regex of -p param, if null then transform all file in dir
       -g  the attr key&value of need find              (eg: key=value or key1=value1|key2=value2|...)
       -v  the attr key&value of not find, opposite -g  (eg: key=value or key1=value1|key2=value2|...)
       -a  the attr key&value of need add               (eg: key=value or key1=value1|key2=value2|...)
       -r  the attr key&value of need replace           (eg: key=value or key1=value1|key2=value2|...)
       -d  the attr key&value of need delete            (eg: key=value or key1=value1|key2=value2|...)
       -t  print fmt. [NULL|0]:line fmt; 1:28 fmt;
       -o  the output dir
       -O  output all cdr not only greped
       -h  help info
       
   Note: use the "'" for key&value param. eg: -g '1=15312002990'
END
    }
}

# 用来处理输入的命令行参数
sub init_param{
    getopts('hOf:p:s:g:a:r:d:o:t:v:F:l:', \%opts);
    
    $log_level = $opts{'l'} if(exists $opts{'l'});
    
    if(exists $opts{'h'} ){
        usage();
        exit(1);
    }
    
    if(exists $opts{'g'}){
        $opts{'g'} =~ s/\s//g;
        @param_g = split /\|/, $opts{'g'};
    }else{
        usage();
        mylog(__LINE__, 'die', 'init_param', 'request param [-g] !');
    }
    
    if( exists $opts{'t'} ){
        mylog(__LINE__, 'warn', 'init_param', "will output on current screen!") if ($opts{'t'} ne 'off');
        mylog(__LINE__, 'warn', 'init_param', "print screen off!") if ($opts{'t'} eq 'off');
    }elsif(exists $opts{'o'}){
        $opts{'o'} =~ s/\s//g;
        if (! -d $opts{'o'}){
            mylog(__LINE__, 'die', 'init_param', "[$opts{'o'}] not exists!");
         }
    }else{
        usage();
        mylog(__LINE__, 'die', 'init_param', 'request param [-t & -o] !');
    }
    
    
    if(exists $opts{'r'}){
        $opts{'r'} =~ s/\s//g;
        @param_r = split /\|/, $opts{'r'};
    }
    
    if(exists $opts{'d'}){
        $opts{'d'} =~ s/\s//g;
        @param_d = split /\|/, $opts{'d'};
    }
    
    if(exists $opts{'a'}){
        $opts{'a'} =~ s/\s//g;
        @param_a = split /\|/, $opts{'a'};
    }
    
    if(exists $opts{'v'}){
        $opts{'v'} =~ s/\s//g;
        @param_v = split /\|/, $opts{'v'};
    }
    
}

# 将hash的话单整理为28格式，将结果记录到一个数组中返回
sub output_28_obj{
    my ($cdr_list_ref, $file_name) = @_;
    mylog(__LINE__, '3', 'create_28_obj', "Begin Create New 28 FMT CDR: $file_name.new");
    
    return 1 if ($opts{'t'} eq 'off');
    
    if(exists $opts{'o'}){
        my $last_dir = getcwd;
        chdir $init_dir or mylog(__LINE__, 'die','output_28_obj',"Cannot chdir to [$init_dir]: $!");
        chdir $opts{'o'} or mylog(__LINE__, 'die','output_28_obj',"Cannot chdir to [$opts{'o'}]: $!");
        
        open OUT_FH, ">>$file_name.new" or mylog(__LINE__, 'die','write_new_cdr',"Cannot open $file_name.new: $!");
        print OUT_FH "{\n";
        
        foreach my $cdr_key (sort {$a <=> $b} keys %$$cdr_list_ref){
            #print $cdr_key, '=', $$$cdr_list_ref{$cdr_key}, "\n";
            printf OUT_FH "\t%s=%s\n", $cdr_key, $$$cdr_list_ref{$cdr_key};
        }
        
        print OUT_FH "}\n";
        close OUT_FH;
        chdir $last_dir or mylog(__LINE__, 'die','output_28_obj',"Cannot chdir to [$last_dir]: $!");
    }elsif(!exists $opts{'t'} || $opts{'t'} eq '0'){
        my $line_cdr = '{';
        foreach my $cdr_key (sort {$a <=> $b} keys %$$cdr_list_ref){
            $line_cdr .= '|' . $cdr_key . '=' . $$$cdr_list_ref{$cdr_key};
        }
        $line_cdr .= '|}';
        print $line_cdr, "\n";
    }elsif($opts{'t'} == 1){
        print "{\n";
        foreach my $cdr_key (sort {$a <=> $b} keys %$$cdr_list_ref){
            printf "\t%s=%s\n", $cdr_key, $$$cdr_list_ref{$cdr_key};
        }
        print "}\n";
    }else{
        mylog(__LINE__, 'die', 'create_28_obj', "unknown output type: [$opts{'t'}]");
    }
}

# 根据参数要求，对符合的话单替换属性值
# hash的实现方式
sub replace_attr{
    my ($cdr_list_ref) = @_;
    mylog(__LINE__, '3', 'replace_attr', "begin replace attr!");
    
    # 根据传入的需要替换的属性，进行替换
    foreach my $p (@param_r){
        my ($pk, $pv) = split /\=/, $p;
        
        # 直接在数组中进行替换
        if(substr($pv,0,1) eq '$'){
            my $pk_a = substr($pv,1);
            # 直接在hash中进行添加
            if(!exists $$$cdr_list_ref{$pk} or !exists $$$cdr_list_ref{$pk_a}){
                mylog(__LINE__, 'warn', 'replace_attr', "this cdr not has key=$pk or not has key=$pk_a , add attr faile!");
            }else{
                $$$cdr_list_ref{$pk} = $$$cdr_list_ref{$pk_a};
            }
        }else{
            if(exists $$$cdr_list_ref{$pk}){
                $$$cdr_list_ref{$pk} = $pv;
            }else{
                mylog(__LINE__, 'warn', 'replace_attr', "this cdr not has key=$pk , replace faile!");
            }
        }
    }
}

# 根据参数要求，对符合的话单删除属性值
sub delete_attr{
    my ($cdr_list_ref) = @_;
    mylog(__LINE__, '3', 'delete_attr', "begin delete attr!");
    
    # 根据传入的需要删除的属性，进行删除
    foreach my $p (@param_d){
        my ($pk, $pv) = split /\=/, $p;
        
        # 直接在hash中进行删除
        if( $pv eq '$' ){
            if(exists $$$cdr_list_ref{$pk}){
                delete $$$cdr_list_ref{$pk};
            }else{
                mylog(__LINE__, '5', 'delete_attr', "this cdr not has key=$pk , not delete !");
            }
        }else{
            if(exists $$$cdr_list_ref{$pk} && $$$cdr_list_ref{$pk} eq $pv){
                delete $$$cdr_list_ref{$pk};
            }else{
                mylog(__LINE__, '5', 'delete_attr', "this cdr key=$pk not eq [$pv], not delete !");
            }
        }
    }
}

# 根据参数要求，添加属性
sub add_attr{
    my ($cdr_list_ref) = @_;
    mylog(__LINE__, '3', 'delete_attr', "begin delete attr!");
    
    # 根据传入的需要添加的属性，进行添加
    foreach my $p (@param_a){
        my ($pk, $pv) = split /\=/, $p;
        
        if(substr($pv,0,1) eq '$'){
            my $pk_a = substr($pv,1);
            # 直接在hash中进行添加
            if(exists $$$cdr_list_ref{$pk} or !exists $$$cdr_list_ref{$pk_a}){
                mylog(__LINE__, 'warn', 'add_attr', "this cdr already has key=$pk or not exists key=$pk_a , add attr faile!");
            }else{
                $$$cdr_list_ref{$pk} = $$$cdr_list_ref{$pk_a};
            }
        }else{
            # 直接在hash中进行添加
            if(exists $$$cdr_list_ref{$pk}){
                mylog(__LINE__, 'warn', 'add_attr', "this cdr already has key=$pk , add attr faile!");
            }else{
                $$$cdr_list_ref{$pk} = $pv;
            }
        }
    }
}

# 针对获取到的文件进行处理，返回处理后的28格式话单
sub proc_one_file{
    my ($file_name) = @_;
    mylog(__LINE__, '3','proc_one_file',"begin transform $file_name!");
    
    open FH, $file_name or mylog(__LINE__, 'die','proc_one_file',"Cannot open $file_name: $!");
    
    # 遍历所有内容，但是只将符合条件的数据放入hash的列表中
    my @cdr_AoH;        # 存放所有匹配到话单的数组
    my %cdr;            # 单条话单按照列表存放
    my $cdr_begin = 0;  # 话单开头标志
    my $is_fix    = 0;  # 判断本条记录是否符合选择需求
    my ($k, $v, $line_num) = ('','',0);
    foreach my $line (<FH>){
        $line_num++;
        chomp $line;
        #mylog(__LINE__, '3','proc_one_file',"get line[$line_num]: $line");
        
        # 将单条记录压入数组，如果没有开始标志则认为话单有问题
        if( $line eq '{' ) {
        #if( substr($line,0,1) eq '{' ) {
            $cdr_begin = 1;
        }elsif($line eq '}'){
            $cdr_begin = 0;
            if($is_fix){
                # 添加多个选择条件的支持，各个选择条件之间用"|"隔开
                # 在之前已经匹配的字典中直接判断是否满足其他条件
                # 同时也进行是否不满足的判断
                if(scalar(@param_g) > 1 and !exists $opts{'O'}){
                    foreach my $p (@param_g){
                        my ($pk, $pv) = split /=/, $p;
                        $is_fix = 0 if (!exists $cdr{$pk} || $cdr{$pk} ne $pv);
                    }
                }
                
                # 判断除外条件，可以多个参数
                foreach my $p (@param_v){
                    my ($pk, $pv) = split /=/, $p;
                    $is_fix = 0 if ($cdr{$pk} eq $pv);
                }
                push @cdr_AoH, {%cdr} if ($is_fix == 1);
            }
            undef %cdr;
            undef $is_fix;
        }elsif( $cdr_begin ){
            #$line    =~ s/^\t//;
            $line = substr($line, 1);
            
            #如果设置了-O参数，则将所有话单都作为已经匹配
            if(exists $opts{'O'}){
                $is_fix  = 1;
            }else{
                $is_fix  = 1 if ($line eq $param_g[0]);
            }
            
            ($k, $v) = split(/=/, $line);
            $cdr{$k} = $v;
        }elsif( $line eq '' ){
            next;
        }else{
            mylog(__LINE__, 'warn','proc_one_file',"cdr fmt err, has not beging '{'! \n\t$line");
            next;
        }
        
#        if($line_num % 10000 == 0){
#            mylog(__LINE__, '5','proc_one_file',"proc data [$line_num] lines!");
#            mylog(__LINE__, '5','proc_one_file', 'greped cdr [' . scalar(@cdr_AoH) . '] !');
#        }
    }
    
    mylog(__LINE__, '3','proc_one_file',"proc all data [$line_num] lines!");
    mylog(__LINE__, '3','proc_one_file', 'greped all cdr [' . scalar(@cdr_AoH) . '] !');
    
    close FH or mylog(__LINE__, 'die','proc_one_file',"Cannot close $file_name: $!");
    
    # 针对匹配到并且生成数组的28话单进行处理
    foreach my $list (@cdr_AoH){
        # 开始处理XX文件的XX条话单
        replace_attr(\$list) if (exists $opts{'r'});
        delete_attr(\$list)  if (exists $opts{'d'});
        add_attr(\$list) if (exists $opts{'a'});
        
        #如果定义了其他函数，在这里调用
        if (exists $opts{'F'}){
            mylog(__LINE__, '3','proc_one_file',"use other fun file: [$opts{'F'}] proc cdr!");
            require $opts{'F'};
            &proc_main(\$list);
        }
        
        #将处理过的数据进行输出
        output_28_obj(\$list, $file_name);
    }
    
    #print Dumper(@cdr_aoa);
}

# 根据传入的参数获取文件
sub proc_file_list{
    my ($path) = @_;
    
    chdir $path or mylog(__LINE__, 'die','proc_file_list',"Cannot chdir to $path: $!");
    
    opendir DH, './' or mylog(__LINE__, 'die','proc_file_list',"Cannot open $path: $!");
    
    # 采用文件目录读取方式，可以支持大量文件列表，但是要求写入正则表达式
    foreach my $file (readdir DH){
        if( -f $file ) {
            if (exists $opts{'s'} && $file =~ /$opts{'s'}/){
                mylog(__LINE__, '3','proc_file_list',"get file in [$path]: $file");
                proc_one_file($file);
            }elsif( !exists $opts{'s'}){
                mylog(__LINE__, '3','proc_file_list',"get file in [$path]: $file");
                proc_one_file($file);
            }
        }
    }
    
    closedir DH or mylog(__LINE__, 'die','proc_file_list',"Cannot close $path DIRHANDLE: $!");
    
    chdir $init_dir or mylog(__LINE__, 'die','proc_file_list',"Cannot open $init_dir: $!");
}

# 主函数
sub main{
    mylog(__LINE__, '3', 'main', 'begin init param!');
    init_param();
    mylog(__LINE__, '3', 'main', 'param init ok!');

    # 判断传入单个文件还是目录
    if (exists $opts{'f'}){
        proc_one_file($opts{'f'});
    }else{
        proc_file_list($opts{'p'});
    }
}

mylog(__LINE__, '3', 'main', 'begin translate 28 fmt cdr!');
main();
mylog(__LINE__, '3', 'main', 'end translate 28 fmt cdr!');

